package com.jichangxiu.framework.filter;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.jichangxiu.common.constant.Constants;
import com.jichangxiu.common.context.JcxContext;
import com.jichangxiu.common.entity.bo.Payload;
import com.jichangxiu.common.entity.bo.Record;
import com.jichangxiu.common.entity.vo.Result;
import com.jichangxiu.common.enums.ErrorType;
import com.jichangxiu.common.utils.JcxContextUtils;
import com.jichangxiu.common.utils.ObjUtils;
import com.jichangxiu.common.utils.ServletUtils;
import com.jichangxiu.framework.entity.bo.SecurityToken;
import com.jichangxiu.framework.entity.bo.SecurityUser;
import com.jichangxiu.common.entity.rule.InfoRule;
import com.jichangxiu.framework.factory.InfoRuleAsyncFactory;
import com.jichangxiu.framework.factory.InfoRuleServiceFactory;
import com.jichangxiu.framework.utils.AsyncUtils;
import com.jichangxiu.framework.utils.InfoRuleUtils;
import com.jichangxiu.framework.utils.SecurityUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;

@Slf4j
public class LoginFilter extends AbstractAuthenticationProcessingFilter {

    public LoginFilter() {
        super(new AntPathRequestMatcher(Constants.JCX_PROPERTIES.getSecurityProperties().getLoginProcessingUrl(), HttpMethod.POST.name()));
    }

    protected void setDetails(HttpServletRequest request, SecurityToken securityToken) {
        securityToken.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException {
        if (!httpServletRequest.getMethod().equals("POST")) {
            throw new RuntimeException("【LoginFilter】不支持【" + httpServletRequest.getMethod() + "】" + "请求方式");
        } else {
            Record record = Record.getRequestRecord(httpServletRequest);
            if (ObjectUtil.isEmpty(record.keySet()) || record.keySet().isEmpty())
                throw new RuntimeException("【LoginFilter】未获取到有效参数");
            String tenantId = record.getStr(Constants.JCX_PROPERTIES.getServerProperties().getTenantIdKeyToServerKey(), "").trim();
            String userInfo = record.getStr(Constants.JCX_PROPERTIES.getServerProperties().getUserInfoToServerKey(), "").trim();
            String password = record.getStr(Constants.JCX_PROPERTIES.getServerProperties().getUserPassToServerKey(), "").trim();
            if (StrUtil.isBlank(tenantId)) throw new RuntimeException("【LoginFilter】【租户信息】为空");
            if (StrUtil.isBlank(userInfo)) throw new RuntimeException("【LoginFilter】【用户名 | 手机号 | 邮箱】为空");
            if (StrUtil.isBlank(password)) throw new RuntimeException("【LoginFilter】【密码】为空");
            // 该处对第一步的token进行包装，用于在AuthenticationProvider里面校验是否该AuthenticationProvider拦截校验
            SecurityToken securityToken = new SecurityToken(tenantId, userInfo, password);
            // 登陆时多租户插件中无法从请求中获取正确的租户ID，所以需要在这里把租户ID设置到请求域
            JcxContextUtils.fillJcxContextEntityByLogin(tenantId, userInfo);
            ServletUtils.setInfoToRequest(httpServletRequest, InfoRuleUtils.LOGIN_INFO_RECORD, InfoRuleUtils.getLoginInfoRule(httpServletRequest, securityToken));
            this.setDetails(httpServletRequest, securityToken);
            return this.getAuthenticationManager().authenticate(securityToken);
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain chain, Authentication authentication) throws IOException {
        InfoRule loginInfoRule = ServletUtils.getInfoFromRequest(httpServletRequest, InfoRuleUtils.LOGIN_INFO_RECORD, InfoRule.class);
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = httpServletResponse.getOutputStream();
        Result<Object> result = null;
        try {
            SecurityUser securityUser = (SecurityUser) authentication.getDetails();
            String jti = SecurityUtils.getJtiFromRedis(securityUser.getTenantId(), securityUser.getUserId());
            Payload<SecurityUser> payload;
            if (StrUtil.isEmpty(jti)) {
                payload = SecurityUtils.login(securityUser);
                jti = payload.getJti();
                securityUser = payload.getUserInfo();
            } else {
                SecurityUtils.verifyJwt(jti);
                payload = SecurityUtils.getPayload(jti);
            }
            JcxContextUtils.fillJcxContextEntityByPayload(payload.getJti(), payload.getJwt());
            JcxContext.setSecurityUser(securityUser);
            if (StrUtil.isEmpty(jti)) throw new RuntimeException("【LoginFilter】【JWT】生成失败");
            Record userRecord = fillSecurityUser(securityUser);
            if (Constants.JCX_PROPERTIES.getSecurityProperties().getWriteJtiToHeader())
                httpServletResponse.setHeader(Constants.JCX_PROPERTIES.getClientProperties().getAuthToClientKey(), jti);
            Record record = Record.getRecord(Constants.JCX_PROPERTIES.getClientProperties().getJtiToClientKey(), jti)
                    .set(Constants.JCX_PROPERTIES.getClientProperties().getUserInfoToClientKey(), userRecord)
                    .set(Constants.JCX_PROPERTIES.getClientProperties().getTenantInfoToClientKey(), securityUser.getTenantRule());
            ObjUtils.formatObject(record);
            result = Result.ok("登录成功", record);
            InfoRuleUtils.fillSuccessLoginInfoRule(loginInfoRule, payload, result);
        } catch (Exception ex) {
            ex.printStackTrace();
            result = Result.no(ErrorType.CREDENTIAL_EXCEPTION).MESSAGE(ex.getMessage());
            InfoRuleUtils.fillFailLoginInfoRule(loginInfoRule, ex, result);
        } finally {
            AsyncUtils.getINSTANCE().execute(InfoRuleAsyncFactory.recordInfoRule(InfoRuleServiceFactory.infoRuleService(), loginInfoRule));
            outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));
        }
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException exception) throws IOException {
        exception.printStackTrace();
        // 从请求域中取到登录日志
        InfoRule loginInfoRule = ServletUtils.getInfoFromRequest(httpServletRequest, InfoRuleUtils.LOGIN_INFO_RECORD, InfoRule.class);
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = httpServletResponse.getOutputStream();
        Result<Object> result = Result.no(exception);
        InfoRuleUtils.fillFailLoginInfoRule(loginInfoRule, exception, result);
        // 异步记录登录日志
        AsyncUtils.getINSTANCE().execute(InfoRuleAsyncFactory.recordInfoRule(InfoRuleServiceFactory.infoRuleService(), loginInfoRule));
        outputStream.write(JSONUtil.toJsonStr(result).getBytes());
        outputStream.flush();
        outputStream.close();
    }

    private Record fillSecurityUser(SecurityUser securityUser) {
        Record userRecord = Record.getRecord();
        if (ObjectUtil.isNotEmpty(securityUser)) {
            userRecord.setModel(securityUser.getUserRule());
            userRecord.remove("password");
            userRecord.remove("loginPwd");
            userRecord.set(Constants.JCX_PROPERTIES.getClientProperties().getPostCodeListToClientKey(), securityUser.getPostCodeList());
            userRecord.set(Constants.JCX_PROPERTIES.getClientProperties().getRoleCodeListToClientKey(), securityUser.getRoleCodeList());
            userRecord.set(Constants.JCX_PROPERTIES.getClientProperties().getPowerCodeListToClientKey(), securityUser.getPowerCodeList());
            userRecord.set(Constants.JCX_PROPERTIES.getClientProperties().getAuthCodeListToClientKey(), securityUser.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
        }
        return userRecord;
    }

}
